using System;
using System.Collections.Generic;
using System.IO;
using iTextSharp.text.pdf;
using System.Text;
using System.Diagnostics;

namespace PdfToText
{
    /// <summary>
    /// Parses a PDF file and extracts the text from it.
    /// </summary>
    public class PDFParser 
    {
        /// BT = Beginning of a text object operator 
        /// ET = End of a text object operator
        /// Td move to the start of next line
        ///  5 Ts = superscript
        /// -5 Ts = subscript

        #region Fields

        #region _numberOfCharsToKeep
        /// <summary>
        /// The number of characters to keep, when extracting text.
        /// </summary>
        private static int _numberOfCharsToKeep = 20; // FRED: increased from 15 to 20 for Tm analysis
        #endregion

        #endregion

        #region ExtractText
        /// <summary>
        /// Extracts a text from a PDF file.
        /// </summary>
        /// <param name="inFileName">the full path to the pdf file.</param>
        /// <param name="outFileName">the output file name.</param>
        /// <returns>the extracted text</returns>
        public void ExtractText(string inFileName, string outFileName)
        {
            Console.Write("Processing...\n");

            Stopwatch sw = new Stopwatch();
            Stopwatch swp = new Stopwatch();
            Stopwatch swits = new Stopwatch();
            sw.Start();

            StringBuilder output = new StringBuilder(1000000);

            swits.Start();
            PdfReader reader = new PdfReader(inFileName);
            int nbrPages = reader.NumberOfPages;
            swits.Stop();

            for (int page = 1; page <= nbrPages; page++)
            {
                if (page > 1) output.Append("\n\n");
                
                swits.Start();
                byte[] pageContent = reader.GetPageContent(page);
                swits.Stop();

                swp.Start();
                ExtractTextFromPDFBytes(pageContent, output);
                swp.Stop();
            }

            sw.Stop();
            Console.Write("Total time = " + sw.ElapsedMilliseconds + " ms\n");
            Console.Write("PDF reading time (iTextSharp) = " + swits.ElapsedMilliseconds + " ms\n");
            Console.Write("Text parsing time (our code) = " + swp.ElapsedMilliseconds + " ms\n");
            Console.Write("Output = \n");
            Console.Write(output.ToString());
            Console.ReadKey();
        }
        #endregion

        #region ExtractTextFromPDFBytes
        /// <summary>
        /// This method processes an uncompressed Adobe (text) object 
        /// and extracts text.
        /// </summary>
        /// <param name="input">uncompressed</param>
        /// <returns></returns>
        private void ExtractTextFromPDFBytes(byte[] input, StringBuilder output)
        {
            if (input == null || input.Length == 0) return;

            // store all found texts in table by line offset (y) and column offset (x)
            Dictionary<decimal, Dictionary<decimal, string>> allTexts = new Dictionary<decimal, Dictionary<decimal, string>>();
            decimal x = 0, y = 0;
            string t = "";

            // Flag showing if we are we currently inside a text object
            bool inTextObject = false;

            // Flag showing if the next character is literal 
            // e.g. '\\' to get a '\' character or '\(' to get '('
            bool nextLiteral = false;

            // () Bracket nesting level. Text appears inside ()
            int bracketDepth = 0;

            // Keep previous chars to get extract numbers etc.:
            char[] previousCharacters = new char[_numberOfCharsToKeep];
            for (int j = 0; j < _numberOfCharsToKeep; j++) previousCharacters[j] = ' ';

            int len = input.Length;
            for (int i = 0; i < len; i++)
            {
                char c = (char)input[i];

                if (inTextObject)
                {
                    // Position the text
                    if (bracketDepth == 0)
                    {
                        // FRED: look for positioning information
                        if (CheckToken(new string[] { "Tm" }, previousCharacters))
                        {
                            string s = new string(previousCharacters).TrimEnd();
                            int sep3 = s.LastIndexOf(' ');
                            int sep2 = s.LastIndexOf(' ', sep3 - 1);
                            int sep1 = s.LastIndexOf(' ', sep2 - 1);
                            string sx = s.Substring(sep1 + 1, sep2 - sep1 - 1);
                            string sy = s.Substring(sep2 + 1, sep3 - sep2 - 1);
                            x = decimal.Parse(sx);
                            y = decimal.Parse(sy);
                        }
                        else if (CheckToken(new string[] { "TD", "Td" }, previousCharacters))
                        {
                            //resultString += "\n\r";
                            t += "\n\r";
                        }
                        else if (CheckToken(new string[] { "'", "T*", "\"" }, previousCharacters))
                        {
                            //resultString += "\n";
                            t += "\n";
                        }
                        else if (CheckToken(new string[] { "Tj" }, previousCharacters))
                        {
                            //resultString += " ";
                            t += " ";
                        }
                    }

                    // End of a text object, also go to a new line.
                    if (bracketDepth == 0 &&
                        CheckToken(new string[] { "ET" }, previousCharacters))
                    {
                        inTextObject = false;
                        //resultString += " ";
                        t += " ";
                    }
                    else
                    {
                        // Start outputting text
                        if ((c == '(') && (bracketDepth == 0) && (!nextLiteral))
                        {
                            bracketDepth = 1;
                            t = "";
                        }
                        else
                        {
                            // Stop outputting text
                            if ((c == ')') && (bracketDepth == 1) && (!nextLiteral))
                            {
                                bracketDepth = 0;

                                // FRED: move the text into the line/column table 
                                Dictionary<decimal, string> line = null;
                                if (!allTexts.TryGetValue(y, out line))
                                {
                                    line = new Dictionary<decimal, string>();
                                    allTexts.Add(y, line);
                                }

                                if (line.ContainsKey(x))
                                {
                                    // append new text to previous one
                                    line[x] = line[x] + t;
                                }
                                else
                                {
                                    line.Add(x, t);
                                }

                                t = "";
                            }
                            else
                            {
                                // Just a normal text character:
                                if (bracketDepth == 1)
                                {
                                    // Only print out next character no matter what. 
                                    // Do not interpret.
                                    if (c == '\\' && !nextLiteral)
                                    {
                                        nextLiteral = true;
                                    }
                                    else
                                    {
                                        if (((c >= ' ') && (c <= '~')) ||
                                            ((c >= 128) && (c < 255)))
                                        {
                                            //resultString += c.ToString();
                                            t += c.ToString();
                                        }

                                        nextLiteral = false;
                                    }
                                }
                            }
                        }
                    }
                }

                // Store the recent characters for 
                // when we have to go back for a checking
                for (int j = 0; j < _numberOfCharsToKeep - 1; j++)
                {
                    previousCharacters[j] = previousCharacters[j + 1];
                }
                previousCharacters[_numberOfCharsToKeep - 1] = c;

                // Start of a text object
                if (!inTextObject && CheckToken(new string[]{"BT"}, previousCharacters))
                {
                    inTextObject = true;
                }
            }
                
            // FRED: return sorted texts
            List<decimal> lines = new List<decimal>(allTexts.Keys);
            lines.Sort();
            lines.Reverse();
            foreach (decimal ly in lines)
            {
                Dictionary<decimal, string> texts = allTexts[ly];
                List<decimal> columns = new List<decimal>(texts.Keys);
                columns.Sort();
                foreach (decimal cx in columns)
                {
                    output.Append(texts[cx]);
                    output.Append(' ');
                }

                output.AppendLine();
            }
        }
        #endregion

        #region CheckToken
        /// <summary>
        /// Check if a certain 2 character token just came along (e.g. BT)
        /// </summary>
        /// <param name="search">the searched token</param>
        /// <param name="recent">the recent character array</param>
        /// <returns></returns>
        private bool CheckToken(string[] tokens, char[] recent)
        {
            foreach(string token in tokens)
            {
                if ((recent[_numberOfCharsToKeep - 3] == token[0]) &&
                    (recent[_numberOfCharsToKeep - 2] == token[1]) &&
                    ((recent[_numberOfCharsToKeep - 1] == ' ') ||
                    (recent[_numberOfCharsToKeep - 1] == 0x0d) ||
                    (recent[_numberOfCharsToKeep - 1] == 0x0a)) &&
                    ((recent[_numberOfCharsToKeep - 4] == ' ') ||
                    (recent[_numberOfCharsToKeep - 4] == 0x0d) ||
                    (recent[_numberOfCharsToKeep - 4] == 0x0a))
                    )
                {
                    return true;
                }
            }
            return false;
        }
        #endregion
    }
}
